
// <copyright file="CryptographyHelper.cs" company="Subhadra Software Solutions">
//   
// </copyright>
// <summary>
//   The cryptography helper.
// </summary>

namespace SubhadraSolutions.Sharp.Utils.Security.Cryptography
{
    using System;
    using System.IO;
    using System.Security.Cryptography;
    using System.Text;

    using SubhadraSolutions.Sharp.Utils.Validation;

    /// <summary>
    /// The cryptography helper.
    /// </summary>
    public static class CryptographyHelper
    {
        #region Constants

        /// <summary>
        ///     Define default max password lengths
        /// </summary>
        private const int DefaultMaxpasswordlength = 10;

        // Define default min and max password lengths.
        /// <summary>
        ///     Define default min password lengths
        /// </summary>
        private const int DefaultMinpasswordlength = 8;

        /// <summary>
        ///     Represents Password Character in UpperCase
        /// </summary>
        private const string PassowrdCHARSUCASE = "ABCDEFGHJKLMNPQRSTWXYZ";

        //// Define supported password characters divided into groups.
        //// You can add (or remove) characters to (from) these groups.
        /// <summary>
        ///     Represents Password Character in Lowercase
        /// </summary>
        private const string PasswordCHARSLCASE = "abcdefgijkmnopqrstwxyz";

        /// <summary>
        ///     Represents Password in Special Characters
        /// </summary>
        private const string PasswordCHARSSPECIAL = "*$-+?_&=!%{}/";

        /// <summary>
        ///     Represents Password in Numeric
        /// </summary>
        private const string PasswordCharsnumeric = "23456789";

        #endregion Constants

        #region Static Fields

        /// <summary>
        ///     salt is used to generate key from the shared key
        /// </summary>
        private static readonly byte[] salt = Encoding.ASCII.GetBytes("o6806642kbM7c5");

        #endregion Static Fields

        #region Public Methods and Operators

        /// <summary>
        /// Decrypt the given string.  Assumes the string was encrypted using
        ///     EncryptStringAES(), using an identical sharedKey.
        /// </summary>
        /// <param name="encPassword">
        /// The text to decrypt.
        /// </param>
        /// <param name="sharedKey">
        /// A password used to generate a key for decryption.
        /// </param>
        /// <returns>
        /// Decrypted Password
        /// </returns>
        public static string DecryptStringAES(string encPassword, string sharedKey)
        {
            Guard.ArgumentNotNullOrEmpty(encPassword, "encPassword");
            Guard.ArgumentNotNullOrEmpty(sharedKey, "sharedKey");

            // Declare the RijndaelManaged object
            // used to decrypt the data.
            RijndaelManaged aesAlg = null;

            // Declare the string used to hold
            // the decrypted text.
            string password;

            try
            {
                // generate the key from the shared secret and the salt
                var key = new Rfc2898DeriveBytes(sharedKey, salt);

                // Create a RijndaelManaged object
                // with the specified key and IV.
                aesAlg = new RijndaelManaged();
                aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
                aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8);

                // Create a decrytor to perform the stream transform.
                ICryptoTransform decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV);

                // Create the streams used for decryption.
                byte[] bytes = Convert.FromBase64String(encPassword);

                using (var memoryStreamDecrypt = new MemoryStream(bytes))
                {
                    using (var cryptoStreamDecrypt = new CryptoStream(memoryStreamDecrypt, decryptor, CryptoStreamMode.Read))
                    {
                        using (var streamRederDecrypt = new StreamReader(cryptoStreamDecrypt))
                        {
                            // Read the decrypted bytes from the decrypting stream
                            // and place them in a string.
                            password = streamRederDecrypt.ReadToEnd();
                        }
                    }
                }
            }
            finally
            {
                // Clear the RijndaelManaged object.
                if (aesAlg != null)
                {
                    aesAlg.Clear();
                }
            }

            return password;
        }

        /// <summary>
        /// Encrypt the given string using AES.  The string can be decrypted using
        ///     DecryptStringAES().  The sharedKey parameters must match.
        /// </summary>
        /// <param name="password">
        /// The text to encrypt.
        /// </param>
        /// <param name="sharedKey">
        /// A password used to generate a key for encryption.
        /// </param>
        /// <returns>
        /// Encrypted password
        /// </returns>
        public static string EncryptStringAES(string password, string sharedKey)
        {
            Guard.ArgumentNotNullOrEmpty(password, "password");
            Guard.ArgumentNotNullOrEmpty(sharedKey, "sharedKey");

            string outStr; // Encrypted string to return
            RijndaelManaged aesAlg = null; // RijndaelManaged object used to encrypt the data.

            try
            {
                // generate the key from the shared secret and the salt
                var key = new Rfc2898DeriveBytes(sharedKey, salt);

                // Create a RijndaelManaged object
                // with the specified key and IV.
                aesAlg = new RijndaelManaged();
                aesAlg.Key = key.GetBytes(aesAlg.KeySize / 8);
                aesAlg.IV = key.GetBytes(aesAlg.BlockSize / 8);

                // Create a decrytor to perform the stream transform.
                ICryptoTransform encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV);

                // Create the streams used for encryption.
                using (var memoryStreamEncrypt = new MemoryStream())
                {
                    using (var cryptoStreamEncrypt = new CryptoStream(memoryStreamEncrypt, encryptor, CryptoStreamMode.Write))
                    {
                        using (var streamWriterEncrypt = new StreamWriter(cryptoStreamEncrypt))
                        {
                            ////Write all data to the stream.
                            streamWriterEncrypt.Write(password);
                        }
                    }

                    outStr = Convert.ToBase64String(memoryStreamEncrypt.ToArray());
                }
            }
            finally
            {
                // Clear the RijndaelManaged object.
                if (aesAlg != null)
                {
                    aesAlg.Clear();
                }
            }

            // Return the encrypted bytes from the memory stream.
            return outStr;
        }

        /// <summary>
        ///     Generates a random password.
        /// </summary>
        /// <returns>
        ///     Randomly generated password.
        /// </returns>
        /// <remarks>
        ///     The length of the generated password will be determined at
        ///     random. It will be no shorter than the minimum default and
        ///     no longer than maximum default.
        /// </remarks>
        public static string GenerateRandomPassword()
        {
            return GenerateRandomPassword(DefaultMinpasswordlength, DefaultMaxpasswordlength);
        }

        /// <summary>
        /// Generates a random password of the exact length.
        /// </summary>
        /// <param name="length">
        /// Exact password length.
        /// </param>
        /// <returns>
        /// Randomly generated password.
        /// </returns>
        public static string GenerateRandomPassword(int length)
        {
            return GenerateRandomPassword(length, length);
        }

        /// <summary>
        /// Generates a random password.
        /// </summary>
        /// <param name="minLength">
        /// Minimum password length.
        /// </param>
        /// <param name="maxLength">
        /// Maximum password length.
        /// </param>
        /// <returns>
        /// Randomly generated password.
        /// </returns>
        /// <remarks>
        /// The length of the generated password will be determined at
        ///     random and it will fall with the range determined by the
        ///     function parameters.
        /// </remarks>
        public static string GenerateRandomPassword(int minLength, int maxLength)
        {
            // Make sure that input parameters are valid.
            if (minLength <= 0 || maxLength <= 0 || minLength > maxLength)
            {
                return null;
            }

            // Create a local array containing supported password characters
            // grouped by types. You can remove character groups from this
            // array, but doing so will weaken the password strength.
            var charGroups = new[] { PasswordCHARSLCASE.ToCharArray(), PassowrdCHARSUCASE.ToCharArray(), PasswordCharsnumeric.ToCharArray(), PasswordCHARSSPECIAL.ToCharArray() };

            // Use this array to track the number of unused characters in each
            // character group.
            var charsLeftInGroup = new int[charGroups.Length];

            // Initially, all characters in each group are not used.
            for (int i = 0; i < charsLeftInGroup.Length; i++)
            {
                charsLeftInGroup[i] = charGroups[i].Length;
            }

            // Use this array to track (iterate through) unused character groups.
            var leftGroupsOrder = new int[charGroups.Length];

            // Initially, all character groups are not used.
            for (int i = 0; i < leftGroupsOrder.Length; i++)
            {
                leftGroupsOrder[i] = i;
            }

            // Because we cannot use the default randomizer, which is based on the
            // current time (it will produce the same "random" number within a
            // second), we will use a random number generator to seed the
            // randomizer.

            // Use a 4-byte array to fill it with random bytes and convert it then
            // to an integer value.
            var randomBytes = new byte[4];

            // This array will hold password characters.

            // Generate 4 random bytes.
            using (var rng = new RNGCryptoServiceProvider())
            {
                rng.GetBytes(randomBytes);

                // Convert 4 bytes into a 32-bit integer value.
                int seed = (randomBytes[0] & 0x7f) << 24 | randomBytes[1] << 16 | randomBytes[2] << 8 | randomBytes[3];

                // Now, this is real randomization.
                var random = new Random(seed);

                // Allocate appropriate memory for the password.
                char[] password = minLength < maxLength ? new char[random.Next(minLength, maxLength + 1)] : new char[minLength];

                // Index of the next character to be added to password.

                // Index of the next character group to be processed.

                // Index which will be used to track not processed character groups.

                // Index of the last non-processed character in a group.

                // Index of the last non-processed group.
                int lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;

                // Generate password characters one at a time.
                for (int i = 0; i < password.Length; i++)
                {
                    // If only one character group remained unprocessed, process it;
                    // otherwise, pick a random character group from the unprocessed
                    // group list. To allow a special character to appear in the
                    // first position, increment the second parameter of the Next
                    // function call by one, i.e. lastLeftGroupsOrderIdx + 1.
                    int nextLeftGroupsOrderIdx = lastLeftGroupsOrderIdx == 0 ? 0 : random.Next(0, lastLeftGroupsOrderIdx);

                    // Get the actual index of the character group, from which we will
                    // pick the next character.
                    int nextGroupIdx = leftGroupsOrder[nextLeftGroupsOrderIdx];

                    // Get the index of the last unprocessed characters in this group.
                    int lastCharIdx = charsLeftInGroup[nextGroupIdx] - 1;

                    // If only one unprocessed character is left, pick it; otherwise,
                    // get a random character from the unused character list.
                    int nextCharIdx = lastCharIdx == 0 ? 0 : random.Next(0, lastCharIdx + 1);

                    //// Add this character to the password.
                    password[i] = charGroups[nextGroupIdx][nextCharIdx];

                    //// If we processed the last character in this group, start over.
                    if (lastCharIdx == 0)
                    {
                        charsLeftInGroup[nextGroupIdx] = charGroups[nextGroupIdx].Length;

                        //// There are more unprocessed characters left.
                    }
                    else
                    {
                        //// Swap processed character with the last unprocessed character
                        //// so that we don't pick it until we process all characters in
                        //// this group.
                        if (lastCharIdx != nextCharIdx)
                        {
                            char temp = charGroups[nextGroupIdx][lastCharIdx];
                            charGroups[nextGroupIdx][lastCharIdx] = charGroups[nextGroupIdx][nextCharIdx];
                            charGroups[nextGroupIdx][nextCharIdx] = temp;
                        }

                        //// Decrement the number of unprocessed characters in
                        //// this group.
                        charsLeftInGroup[nextGroupIdx]--;
                    }

                    //// If we processed the last group, start all over.
                    if (lastLeftGroupsOrderIdx == 0)
                    {
                        lastLeftGroupsOrderIdx = leftGroupsOrder.Length - 1;

                        //// There are more unprocessed groups left.
                    }
                    else
                    {
                        //// Swap processed group with the last unprocessed group
                        //// so that we don't pick it until we process all groups.
                        if (lastLeftGroupsOrderIdx != nextLeftGroupsOrderIdx)
                        {
                            int temp = leftGroupsOrder[lastLeftGroupsOrderIdx];
                            leftGroupsOrder[lastLeftGroupsOrderIdx] = leftGroupsOrder[nextLeftGroupsOrderIdx];
                            leftGroupsOrder[nextLeftGroupsOrderIdx] = temp;
                        }

                        //// Decrement the number of unprocessed groups.
                        lastLeftGroupsOrderIdx--;
                    }
                }

                //// Convert password characters into a string and return the result.
                return new string(password);
            }
        }

        #endregion Public Methods and Operators
    }
}
